docs(spec): three prose carriers state what the tree does, not a premise it falsifies - #16930
Conversation
…premise it falsifies `field-value.zod.ts`'s `FILE_REFERENCE_TYPES` docblock still said the stored schema "deliberately admits both until D3 lands" while the classifier has returned `FileReferenceIdValueSchema` alone for `form === 'stored'` since ADR-0104 D3 wave 2. `protocol.zod.ts`'s `CreateManyDataResponseSchema.droppedFields` justified its aggregated shape with "schema-uniform, so every row drops the same set", a premise ruling C (#14147) falsifies by exempting keys a `beforeInsert` hook wrote, tracked per row. `filter.zod.ts`'s `FieldReferenceSchema` `@example` spelled the #14104 shape as the relation path `duty.grace_days`, 19 lines above the prose in the same block saying a dotted path is refused. Each sentence is rewritten to what the code does today, citing the landing that made it true rather than a schedule. No predicate, schema arm, key or export moves. Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x Co-authored-by: Claude <noreply@anthropic.com>
The `droppedFields` correction is a `.describe()`, so it lands in the JSON Schema and `content/docs/references/api/protocol.mdx`; `check:generated` proved exactly that one artifact stale and `--fix` regenerated only it. Changeset is `patch` on `@objectstack/spec`: all three edited files ship in the package's published `files[]` via `src/**/*.zod.ts`, measured with `npm pack --dry-run`. Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x Co-authored-by: Claude <noreply@anthropic.com>
…ale-contract-prose-trio
…ds string `check:doc-authoring` refuses an internal issue id inside customer-facing spec text, and the rewritten `.describe()` had put one there: a `.describe()` string projects into `content/docs/references/**` and the generated skill artifacts, whose readers have no tracker to resolve `#NNNN` against. The id moves to an adjacent `//` comment — the placement the gate itself prescribes for a reference that is load-bearing for an internal reader — and the customer-facing sentence now states the mechanism directly instead of citing the ruling that produced it, which is what a caller reading the field can act on. Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 2 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 131 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b39f42dea72086ef6501e7f6553252a77d7132dd && git checkout b39f42dea72086ef6501e7f6553252a77d7132dd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9a89a0040de98fc77046c5b405db1a3935fd7b18 7aec120c327a4bfcd67db6cc8ff0b88b9fa1c6fd && git checkout -B drift-repro 9a89a0040de98fc77046c5b405db1a3935fd7b18 && git merge --no-ff 7aec120c327a4bfcd67db6cc8ff0b88b9fa1c6fd
node scripts/docs-audit/affected-docs.mjs --json 9a89a0040de98fc77046c5b405db1a3935fd7b18
|
ACCEPT —
|
| card | grep | origin/main |
PR head | control (main → head) |
|---|---|---|---|---|
| #15769 | deliberately admits both until D3 lands |
1 | 0 | FILE_REFERENCE_TYPES 2 → 2 |
| #15777 | schema-uniform |
1 | 0 | droppedFields 6 → 6 |
| #15778 | duty.grace_days |
2 | 0 | FieldReferenceSchema 13 → 13 |
Other readings this seat took
| reading | value | instrument |
|---|---|---|
| diff shape | 5 files, +40 / −10 | git diff --stat 70f7d6d73 7aec120c3 |
| clause-② pairs | exit 0 on all three — declaration legible, carriers agree, no widening tell | check-clause2-carriers.mjs --pair 16930 |
| governed surface | 0 of 5 paths hit the register, re-run on the FINAL list | check-governed-merges.mjs --test |
| model tier | 305 harness-stamped "model":"claude-opus-5", no other value |
subagent transcript grep — ⛔ never a self-description |
The one content/docs file in the diff is references/api/protocol.mdx, generated by gen:docs because a .describe() moved — one table row. ⛔ No hand-written page was edited.
⭐ The docs-drift advisory answered as readings, and the answer is the interesting part
The advisory listed 18 rows (16 hand-written + 2 release-owned). The dev re-derived them on a tree it can name — head 7aec120c3 with an empty git status --porcelain, reporting dirty:false, unlike the advisory's own checkout which reported carrying uncommitted changes — and got the same 18 rows. Then, per member:
- [finding]
CreateManyDataResponseSchema.droppedFieldsjustifies its aggregated shape withschema-uniform, so every row drops the same set— ruling C's per-row hook exemption falsifies that premise #15777, verbatim AND paraphrase. Across all ofcontent/docs:schema-uniform0,every row drops0,drops the same set0, against live controls (readonly48 pages,bulk69,createMany8). Because a paraphrase counts, it then read all six hand-written pages that mentiondroppedFieldsat all — none makes a per-row or uniformity claim. The only carrier was the generated page, whichgen:docsrewrote here. - [finding] field-value.zod.ts docblock over FILE_REFERENCE_TYPES still says the stored schema "deliberately admits both until D3 lands" — the classifier has admitted only the sys_file id since D3 wave 2 PR-5a (17.0.0) #15769 was anchorless — the advisory said pages documenting
field-value.zod.tsare NOT COVERED. ⭐ Hand-checked rather than read as a clean bill: the hand-written carrier isprotocol/objectql/types.mdxand it is already correct (:1116teaches the opaquesys_fileid and the expanded read form;:1132-1135records that pre-migration deployments may still hold the inline object and the engine warns rather than rejects). ⇒ this change removes a disagreement rather than creating one. - [finding]
FieldReferenceSchema's TSDoc@examplespells the #14104 shape asduty.grace_days— a relation path SQL push-down refuses — two paragraphs above the prose that says so #15778 elsewhere:duty.grace_daysacross all tracked files → 8, and all 8 either record the refusal or test it. Strongest reading: a regex for a$fieldcomparand naming a dotted path across hand-writtencontent/docs/**/*.mdx→ 0, with a live control of 6 same-table$fieldspellings from the same regex minus the dot requirement. - ⛔ Release-owned pages: nothing touched, and neither is wrong — checked, not assumed. Both are coincidental token matches:
v16.mdxmatchesaddDaysbecause it names the formula functionaddDays(d,n), a different surface from the filter offset key;v17.mdxmatchesdue_dateinside aconditionalRequiredexample, and its owndroppedFieldspassage makes no uniformity claim. ⇒ no docs-only card to route.
⇒ Zero pages falsified, so zero pages edited, and every page left alone has a stated reason. That is the right shape of answer.
⚠️ Two things reported honestly that a lesser report would have buried
check:doc-authoringexited 1 against the dev's own diff — an issue id had ended up inside a customer-facing.describe(). It moved the id to an adjacent//comment and re-ran green (15159 customer-facing string(s) across 874 spec sources clean). A gate catching the fix's own new defect, and fixed rather than worked around.- The first commit
8523c22f4was made withcore.hooksPath=/dev/null, skipping pre-commit. Self-reported rather than left implicit. Assessed here: that commit is on the dev's own branch in its own worktree (git branch --contains→claude/issue-15769-stale-contract-prose-trioonly), so the shared-checkout and shared-stash guards it bypassed had nothing to protect; every later commit ran hooks, pre-push passed, and — decisively — the delivered tree is what this review measured, independently, above. ⛔ Not a clean practice and it should not recur; ⛔ not a reason to reject a tree that verifies.
Gates
93 derived, 93 run, 0 UNRUN, reconciliation exit 0. 92 green; 1 NOT MEASURED (check:dual-build-cjs-loads, exit 3 PREREQUISITE NOT MET — needs a whole-repo build), declared as not-measured and ⛔ never counted green. check:generated proved exactly one artifact stale, --fix regenerated only it, and check:api-surface / check:authorable-surface / check:export-origins stayed green throughout — the mechanical half of "no surface moved".
pnpm lint was narrowed to the 3 changed .ts files. The dev backed it on three readings — population taken from eslint's own config rather than guessed, file count read from --format json, and invariance stated by the config's measured header (eslint.config.mjs:327-329: this repo never enables type-aware linting for any file), so this diff cannot move an untouched file's verdict. CI runs the full farm regardless.
Handed back, and now filed
- [finding]
FieldReferenceSchema's FIRST TSDoc@examplespells a$fieldcomparand as the relation pathorder.owner_id— the same block's prose says a dotted path is refused with INVALID_FILTER #16923 (dev-filed, bare): the first@examplein the sameFieldReferenceSchemablock spells a$fieldcomparand as the relation pathorder.owner_id— the same trap [finding]FieldReferenceSchema's TSDoc@examplespells the #14104 shape asduty.grace_days— a relation path SQL push-down refuses — two paragraphs above the prose that says so #15778 records, at a site [finding]FieldReferenceSchema's TSDoc@examplespells the #14104 shape asduty.grace_days— a relation path SQL push-down refuses — two paragraphs above the prose that says so #15778 does not name. ⛔ Not fixed here because it is not mechanical: its caption frames it as a JOIN ON clause, so the correction is a judgement about the join surface. - [finding] packages/metadata-protocol carries the same falsified "schema-uniform — every row drops the same set" justification that #15777 is removing from packages/spec #16933 (filed by this seat, bare):
packages/metadata-protocol/src/protocol.ts:1747carries the same falsifiedschema-uniform — every row drops the same setsentence [finding]CreateManyDataResponseSchema.droppedFieldsjustifies its aggregated shape withschema-uniform, so every row drops the same set— ruling C's per-row hook exemption falsifies that premise #15777 removes here. Verified onorigin/mainby this seat with a control (mergeDroppedFieldEvents→ 6). ⛔ Not folded in, because it would have added a package to this PR's verification surface and broken the three-member cap that keeps each card independently reviewable.
Landing
⛔ Not enqueued yet — checks are still running on 7aec120c3 (31 names seen, 14 in flight, 0 non-green). Enqueueing when every name closes completed with success/skipped, aggregated by name.
Generated by Claude Code
The os-regen driver merged `content/docs/references/api/protocol.mdx` with exit 0 while keeping one side; regenerating from the merged sources restores both — main's `droppedFields` prose (#16930) and this branch's `dashboards` row gaining its `globalFilters` continuation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018rzQyhLGC5iVs11V3TzRs5
Fixes #15769
Fixes #15777
Fixes #15778
Clause-②: no
Three prose carriers inside
packages/spec/src/**each asserted, in the present tense, a premise the same package's own code contradicts. One fix method for all three: rewrite the sentence to what the tree does, leaving the code alone. No predicate, schema arm, key or export moves —check:api-surface,check:authorable-surfaceandcheck:export-originsall pass unchanged, which is the mechanical half of that claim.What each member says now, and the reading that shows the criterion met
Every grep below was taken on the delivered tree; every zero carries a positive control on the same file, so a zero is a reading rather than a dead instrument.
#15769 —
packages/spec/src/data/field-value.zod.tsThe docblock over
FILE_REFERENCE_TYPESsaid the stored schema "deliberately admits both until D3 lands". It has been false since ADR-0104 D3 wave 2: the classifier returnsFileReferenceIdValueSchemaALONE forform === 'stored'and the[FileReferenceIdValueSchema, FileValueSchema]union only for'expanded', andfield-value.test.tspins both directions ("D3 wave 2: the STORED media form is an opaque sys_file id", "…the EXPANDED media form is the resolved object, or a still-unresolved id").The new sentence states the shipped contract and names the landing that made it true — ADR-0104 D3 wave 2, an artefact in the tree at
docs/adr/0104-field-runtime-value-shape-contract.md— rather than a schedule that expires. It also says where legacy stored values ARE admitted: the warn-first ADR-0104 value-shape path in@objectstack/objectql's record validator, never this schema.deliberately admits both until D3 lands:180)FILE_REFERENCE_TYPES→ 2Classifier untouched, verified after the edit:
if (FILE_REFERENCE_TYPES.has(t))still returns the union only underform === 'expanded'.#15777 —
packages/spec/src/api/protocol.zod.tsThe
droppedFields.describe()justified its aggregated shape with "schema-uniform, so every row drops the same set". Ruling C (#14147) falsifies that premise, and the mechanism is onorigin/maintoday:engine.insertcallsstripReadonlyFieldsper row withhookWrittenKeys: rowHookWrittenKeys[i], so abeforeInserthook that stamps a protected key on some rows and not others makes those rows drop different sets.The shape is unchanged, and it stays right for the reason the producer itself already gives (
packages/metadata-protocol/src/protocol.ts,createManyData): "the{ records, count }response has no per-row slot, so a union is the only representable view here." The description now says that, and tells a reader how to read a name in the array — "at least one row dropped this field", not "every row did".schema-uniform, so every row:2361)droppedFields→ 6schema-uniformanywhere in fileAGGREGATED→ 1Counts re-measured here, not inherited. On this tree
packages/objectql/src/engine.tsreadsrowHookWrittenKeys5 raw / 4 de-commented, with the controlhookWrittenKeysat 12 raw / 7 de-commented. The card measured 5 on PR #15395's branch head and triage measured 3 onorigin/mainat the time; both readings are explicable and neither is corrected here, because the argument never depended on the count — the load-bearing fact is that the exemption is tracked PER ROW, and all 4 de-commented occurrences are code.The live neighbour, answered.
e08892daclandedCloneDataResponseSchema.droppedFieldson this same file after the card was written. It does NOT carry the same false justification. Reading:grep -c 'schema-uniform' packages/spec/src/api/protocol.zod.tswas 1 before this change, at theCreateManyDataResponseSchemasite only.CloneDataResponseSchemais a single-record create face — its response is{ object, id, sourceId, record }— so it has no aggregation to justify and its.describe()makes no per-row claim at all; it says only that a non-system clone cannot seed a staticreadonlycolumn whether the value was copied or supplied throughoverrides. Nothing to fix there, and it is left untouched.#15778 —
packages/spec/src/data/filter.zod.tsThe
FieldReferenceSchema@examplespelled the #14104 shape as the relation pathduty.grace_days, 19 lines above the same block's prose stating that SQL push-down compiles same-table columns only and refuses a dotted path withINVALID_FILTER. The example is now the same-table spelling that both execution paths compile, matching the wording the hand-written guide already publishes atcontent/docs/protocol/objectql/query-syntax.mdx:480-482. The block's own Execution support prose remains the statement of what a dotted path does, so nothing here teaches the refused spelling.duty.grace_days:40,:41)FieldReferenceSchema→ 13The reason given for this fix is the source-reading channel — an author or AI reading the TSDoc or an IDE hover copies the dotted spelling,
matchesFilterwalks the path so it passes in memory, and SQL push-down answers 400. It is deliberately not argued from a generated reference page: measured here with a live control,content/docs/references/data/filter.mdxis 169 lines, carries the schema's import at:35and a## FieldReferencesection at:56, and returns 0 hits forgrace_days,owner_id,completed_atanddue_date. Neither@examplein this block reaches that page.Changeset: measured, not assumed
A real changeset (
patchon@objectstack/spec), notskip-changeset. The hypothesis that a TSDoc-only edit publishes nothing is false for this package:@objectstack/specships its own sources.packages/spec/package.jsonfiles[]containssrc/**/*.zod.ts.npm pack --dry-run --jsoninpackages/spec: 2073 published entries;src/data/field-value.zod.ts,src/api/protocol.zod.tsandsrc/data/filter.zod.tsare all among them. Positive control that the glob actually filters rather than shipping everything: exactly one entry undersrc/is not a.zod.tsfile (src/migrations/entries/README.md)..describe()additionally reaches the built bundle and the reference page:grep -rl 'no per-row slot to hang a' packages/spec/dist/→dist/api/index.jsanddist/api/index.mjs; andcheck:generatedproved exactly one artifact stale,content/docs/references/**, regenerated with--fixto a single changed table row incontent/docs/references/api/protocol.mdx.content/docs/references/data/filter.mdxandcontent/docs/references/data/field-value.mdxare both unmodified by the regeneration.验收备注 (acceptance notes)
Triage's route choice on #15769, stated explicitly as it asked. Triage offered two routes and asked the implementer to say which was taken: 「⭐ 或者只改 docblock、把
FileLikeValueSchema的去留留成另一张卡 —— 那样本卡就是纯文档、零契约风险。⛔ 请在 PR 里明说选了哪种。」 This PR takes the docblock-only route.FileLikeValueSchemastays exported and untouched. Removing it would narrow a published surface, which is a Clause-② contract-review diff, and this dispatch is fenced against widening. Its own docblock is already correct — it is@deprecatedand points atFileReferenceIdValueSchemaas the stored form — so it states nothing false. Measured, with a live control:git grep FileLikeValueSchema -- 'packages/**/src/**'finds only its definition and the spec's own re-export, while the same grep forFileValueSchemafinds real consumers; the zero is a reading. That is dead-code observation, not a defect class this repo files, so it is noted, not filed. Successor: none is queued — recorded here so the next editor of this file does not have to re-measure it.Out of scope, filed: #16923. The FIRST
@examplein the sameFieldReferenceSchemablock spells a{ $field }comparand as the relation pathorder.owner_id, which is the same trap #15778 records at a site #15778 does not name. It is not fixed here because it is not mechanical: its caption frames it as a JOIN ON clause, so choosing between a same-table re-spelling, dropping the join framing, or labelling the memory-only context is a judgement about the join surface rather than a rewrite. Filed unassigned and unlabelled.Out of scope, noted, not filed.
packages/metadata-protocol/src/protocol.ts, the docblock onmergeDroppedFieldEvents, carries the same falsified sentence as #15777 — "The create-side static-readonlystrip is schema-uniform — every row drops the same set — which makes an aggregated view faithful rather than lossy" — softened by a parenthetical noting that since #14147 the strip reports one event per call. It is left alone because correcting it would addpackages/metadata-protocolto this PR's verification surface, and this fold was capped at three members to keep each independently reviewable. Dedup run before deciding: one targetedsearch_issuescall (REST/search/*answers 403 in this container) returned #15777, #14147 and #3455 as the live positive control and no card recording this site. This is the "同类还有几处" answer #15777's triage asked the spec seat for: withinpackages/spec, one —CloneDataResponseSchema, checked above and clean; outside it, this one.A gate caught this diff's own first draft, and the fix is in. The rewritten
.describe()initially cited the ruling by issue id.pnpm check:doc-authoringrefused it: a.describe()string projects intocontent/docs/references/**and the generated skill artifacts, and those readers have no tracker, so#NNNNis a citation-shaped token resolving to nothing. The id now sits in an adjacent//comment — the placement that gate prescribes for a reference load-bearing to an internal reader — and the customer-facing sentence states the mechanism directly. Re-run green: 15159 customer-facing strings across 874 spec sources clean,describebucket 8742.Docs Drift Check — answered with readings, on a tree I can name
Taken in
/home/user/objectstack-issue-15769at HEAD7aec120c3withgit status --porcelainempty, against base9a89a0040. My own re-derivation reports"dirty": false, so unlike the advisory's checkout this reading fully identifies what was read. Same pair, same 18 rows (16 hand-written + 2 release-owned) — a matching list, independently derived.What the 18 rows actually are.
node scripts/docs-audit/affected-docs.mjs --json 9a89a0040…yields four anchors: the symbolCreateManyDataResponseSchemaand the literalsaddDays,due_date,grace_days. The symbol anchor matched zero pages — no hand-written page names that schema. Sixteen of the eighteen rows matched ondue_dateoraddDaysalone: ordinary example column names in my@example, not pages restating anything. Onlycontent/docs/protocol/objectql/query-syntax.mdxmatched on all three literals.1. #15777 — does any page restate the
droppedFieldsjustification, or paraphrase it? No. Over the wholecontent/docstree:schema-uniform→ 0,every row drops→ 0,drops the same set→ 0; live controls on the same corpus,readonly→ 48 pages,bulk→ 69,createMany→ 8. Because a paraphrase counts, I then read every hand-written page that mentionsdroppedFieldsat all (droppedFields→ 6 pages) rather than trusting the string search:protocol/kernel/http-protocol.mdx:1013-1015(which channel carries it),api/data-api.mdx:358-364(the single-record clone body),kernel/contracts/data-engine.mdx:392-396("wherever their contract declares one"). None makes a per-row or uniformity claim of any kind. The generatedreferences/api/batch.mdxis also clean —schema-uniform/every row/same set→ 0 each, against its own live control ofdroppedFields→ 5. The only page that carried the premise was the generatedreferences/api/protocol.mdx, whichgen:docsrewrote in this PR.2. #15769 — the anchorless file, checked by hand.
field-value.zod.tsyields no anchor, so the run explicitly does not cover pages documenting it; I did not read that as a clean bill. The hand-written page that teaches this surface iscontent/docs/protocol/objectql/types.mdx, and it is already correct — it says what my corrected docblock now says::1116"Stored value: an opaquesys_fileid string. The expanded read form is the media metadata object, whose only required member isurl", and the callout at:1132-1135"Deployments predating the file-as-reference migration may still hold the inline metadata object (or a bare URL) as the stored value. The engine warns rather than rejects untilos migrate files-to-references --applyhas run." Both halves of the corrected docblock — stored is the id, legacy values are admitted warn-first by the RUNTIME — are already published there. So the docs were right and the spec docblock was the outlier; this change removes a disagreement rather than creating one. Deliberately left alone, because it is already correct. Corpus readings behind that:sys_file→ 14 pages,file-as-reference→ 4,ADR-0104→ 12 (controlsfile→ 185,attachment→ 28,image→ 44);FILE_REFERENCE_TYPES→ 0 pages, so no page repeats the constant's docblock.content/docs/data-modeling/field-types.mdxcites ADR-0104 only forlocation/address(D1) and hassys_file→ 0, so it makes no claim about the file stored form.3. #15778 — does any OTHER page spell the shape as a relation path? No page teaches it.
duty.grace_daysacross all tracked files → 8 (controlgrace_days→ 14), and every one of the 8 either records the refusal or tests it:query-syntax.mdx(states it is refused withINVALID_FILTER),driver-sql/src/cross-field-conformance-cases.ts:853("SQL push-down refuses it … loudly"),formula/…/matches-filter-field-reference-offset.test.ts, twoservice-analyticsboundary tests,spec/src/data/filter.test.ts:1739,packages/spec/CHANGELOG.md(history), and this PR's own changeset (describing the defect). The blanket reading is the strongest one: a regex for a$fieldcomparand naming a DOTTED path across hand-writtencontent/docs/**/*.mdxreturns 0 hits, with a live positive control of 6 same-table$fieldspellings from the same regex without the dot requirement. ⛔ Triage's measurement that neither@examplereachesreferences/data/filter.mdxis not re-litigated here.Release-owned pages: nothing is wrong, and nothing is edited.
content/docs/releases/**is read-only for a code PR. Both rows are coincidental token matches, checked rather than assumed:releases/v16.mdx:731,1009matches onaddDaysbecause it names the formula functionaddDays(d, n), a different surface from the filteraddDaysoffset key;releases/v17.mdx:556matches ondue_dateinside aconditionalRequired→requiredWhenmigration example, and its owndroppedFieldspassage at:1579-1584makes no uniformity claim. Neither is factually wrong about any of these three defects, so there is no docs-only card to file. The same reasoning applies to theaddDaysrows ondata-modeling/fields.mdx:407anddata-modeling/formulas.mdx:177,228— the formula helper, not the filter key.Net: zero doc pages are falsified by this change, so no page is edited. Every page above was left alone for a stated reason, and the one page that overlaps this change most (
types.mdx) was left alone because it already agrees with the corrected source.What is deliberately NOT claimed
FieldReferenceSchema's TSDoc@examplespells the #14104 shape asduty.grace_days— a relation path SQL push-down refuses — two paragraphs above the prose that says so #15778's card claim that the@exampleis "published twice from one source" is not restated, in this body, the commit message or the changeset. Re-measured here and it does not hold — see the reading above.CreateManyDataResponseSchema.droppedFieldsjustifies its aggregated shape withschema-uniform, so every row drops the same set— ruling C's per-row hook exemption falsifies that premise #15777's card was careful to say.Generated by Claude Code